package net.nycjava.skylight1.dependencyinjection;

import static java.lang.String.format;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;

import javax.inject.Inject;
import javax.inject.Named;

import net.nycjava.skylight1.dependencyinjection.DependencyInjectingObjectFactory.DependencyKey;

/**
 * Given a DependencyInjectingObjectFactory, injects dependencies into a class.
 */
public class DependencyInjector {
    private DependencyInjectingObjectFactory dependencyInjectingObjectFactory;

    public DependencyInjector(DependencyInjectingObjectFactory aDependencyInjectingObjectFactory) {
        dependencyInjectingObjectFactory = aDependencyInjectingObjectFactory;
    }

    @SuppressWarnings("unchecked")
    public <T> void injectDependenciesForClassHierarchy(T anObject) {
        // the reflection API requires that each class in the hierarchy be considered
        // start with the lowest class in the hierarchy
        Class<?> interfaceOfObject = anObject.getClass();

        do {
            // inject the dependencies for this class
            injectDependenciesForSingleClass(anObject, (Class<T>) interfaceOfObject);

            // move on up the class hierarchy...
            interfaceOfObject = interfaceOfObject.getSuperclass();

            // until the top is reached
        } while (interfaceOfObject != null);
    }

    private <T, S extends T> void injectDependenciesForSingleClass(S anObject, Class<T> aClass) {
        // for each
        for (Field field : aClass.getDeclaredFields()) {
            if (field.isAnnotationPresent(Inject.class)) {
                field.setAccessible(true);
                try {
                    // if the field has not already been set (possibly through injection)...
                    if (field.get(anObject) == null) {
                        Class<?> classOfDependency = field.getType();
                        final Object injectedValue;
                        Named named = field.getAnnotation(Named.class);
                        String name = named == null ? null : named.value();
                        DependencyKey key = new DependencyKey(classOfDependency, name);
                        ObjectSource<Object> objectSource = dependencyInjectingObjectFactory.getObjectSource(key);
                        if (objectSource != null) {
                            final int modifiers = classOfDependency.getModifiers();
                            if (objectSource.needLazy
                                    && (Modifier.isInterface(modifiers) || Modifier.isAbstract(modifiers))) {
                                injectedValue = createProxy(key, objectSource);
                            } else {
                                objectSource.needLazy = false;
                                injectedValue = objectSource.getObject(this);
                            }
                            field.set(anObject, injectedValue);
                        } else {
                            // not optional than throw exception
                            throw new RuntimeException(format("No object source was registered for key %s.", key));
                        }
                    }
                } catch (IllegalArgumentException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(format("Unable to access field %s.", field.getName()), e);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T createProxy(final DependencyKey key, final ObjectSource<T> objectSource) {
        return (T) Proxy.newProxyInstance(DependencyInjectingObjectFactory.class.getClassLoader(),
                new Class[] { key.clz }, new InvocationHandler() {
                    private T object;

                    public Object invoke(Object aProxy, Method aMethod, Object[] anArrayOfArguments) throws Throwable {
                        if (object == null) {
                            synchronized (this) {
                                if (object == null) {
                                    objectSource.needLazy = false;
                                    object = (T) objectSource.getObject(DependencyInjector.this);
                                }
                            }
                        }
                        try {
                            return aMethod.invoke(object, anArrayOfArguments);
                        } catch (InvocationTargetException e) {
                            Throwable target = e.getCause();
                            throw target;
                        }

                    }
                });
    }

}
